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

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import blanco.properties.manifest.BlancoPropertiesManifestReader;
import blanco.properties.manifest.valueobject.BlancoPropertiesManifest;
import blanco.properties.message.BlancoPropertiesMessage;

/**
 * 指定ディレクトリ内のプロパティファイルを順次処理するためのパーサ。
 * 
 * @author IGA Tosiki
 */
public abstract class BlancoPropertiesDirectoryParser {
    /**
     * メッセージオブジェクト。
     */
    protected final BlancoPropertiesMessage fMsg = new BlancoPropertiesMessage();

    /**
     * 基準となる入力ディレクトリ。このディレクトリ内にプラグインが含まれます。
     */
    protected File fSourceDir = null;

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

    /**
     * 処理対象のロケール。
     */
    protected String fTargetLocale = "ja";

    /**
     * 処理対象ロケールを考慮しながら処理を進めるかどうか。
     */
    protected boolean fIsProcessTargetLocale = false;

    /**
     * note to translators を報告するかどうか。
     */
    protected boolean fIsReportNoteToTranslators = true;

    /**
     * 報告済みURI
     */
    protected Map<String, String> fReportNoteToTranslatorsResultURI = new HashMap<String, String>();

    /**
     * ターゲット・ロケールを処理対象とするかどうか。
     * 
     * 言語パックから翻訳抽出などの特殊な場合にのみtrueを設定します。
     * 
     * @param argIsProcessTargetLocale
     */
    public void setProcessTargetLocale(final boolean argIsProcessTargetLocale) {
        fIsProcessTargetLocale = argIsProcessTargetLocale;
    }

    public void setTargetLocale(final String arg) {
        fTargetLocale = arg;
    }

    /**
     * note to translators が存在する場合に報告するかどうか。
     * 
     * @param argReportNoteToTranslators
     */
    public void setReportNoteToTranslators(
            final boolean argReportNoteToTranslators) {
        fIsReportNoteToTranslators = argReportNoteToTranslators;
    }

    /**
     * プロパティファイルを処理開始。
     * 
     * @param bundleURI
     * @param inputProps
     * @param argPropertiesNameWithPath
     *            入力元のプロパティファイル名。先頭の/を含みます。_jaなどは付いていない状態となります。
     * @param bufPropsWriter
     * @throws IOException
     */
    protected abstract void beginProperties(final String bundleURI,
            final Properties inputProps,
            final String argPropertiesNameWithPath,
            final BufferedWriter bufPropsWriter) throws IOException;

    /**
     * プロパティファイルのキーを処理。
     * 
     * @param bundleURI
     * @param inputProps
     * @param key
     * @param bufPropsWriter
     * @return 処理したかどうか。
     * @throws IOException
     */
    protected abstract boolean processPropertiesKey(final String bundleURI,
            final Properties inputProps, final String key,
            final BufferedWriter bufPropsWriter) throws IOException;

    /**
     * プロパティファイルを処理終了。
     * 
     * @param bundleURI
     * @param inputProps
     * @param bufPropsBytes
     *            出力先のプロパティファイルの内容。(プロパティファイル出力が必要な場合にのみ利用します)
     * @param isReplaced
     *            置換されたかどうか。
     * @throws IOException
     */
    protected abstract void endProperties(final String bundleURI,
            final Properties inputProps,
            final BlancoPropertiesManifest manifest,
            final byte[] bufPropsBytes, final boolean isReplaced)
            throws IOException;

    /**
     * パースのためのエントリポイント。
     * 
     * @param fileSourceDir
     * @throws IOException
     * @throws IllegalArgumentException
     */
    public void parse(final File fileSourceDir) throws IOException,
            IllegalArgumentException {
        fSourceDir = fileSourceDir;
        final File[] files = fSourceDir.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());
                    }
                }
            }
        }
    }

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

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

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

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

        final File[] files = new File(fSourceDir.getAbsolutePath() + "/"
                + targetPluginFullPlus
                + (subdir.length() == 0 ? "" : "/" + subdir)).listFiles();
        if (files != null) {
            for (int index = 0; index < files.length; index++) {
                if (files[index].isDirectory()) {
                    // TODO ディレクトリを処理しなくても良いのかどうか検討すべし。
                    if (files[index].getName().equals(".")
                            || files[index].getName().equals("..")) {
                        // 処理しない
                    } else {
                        // 再帰呼び出し
                        processPluginExpandedDir(targetPluginFullPlus, subdir
                                + "/" + files[index].getName());
                    }
                } else {
                    if (files[index].getName().endsWith(".jar")) {
                        final InputStream inStream = new BufferedInputStream(
                                new FileInputStream(fSourceDir
                                        .getAbsolutePath()
                                        + "/"
                                        + targetPluginFullPlus
                                        + (subdir.length() == 0 ? "" : "/"
                                                + subdir)
                                        + "/"
                                        + 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(fSourceDir
                                .getAbsolutePath()
                                + "/" + targetPluginFullPlus)
                                .getCanonicalPath();
                        final String strPropertyFilePath = files[index]
                                .getCanonicalPath().substring(
                                        wrkBase.length() + 1);
                        // 報告用ベースディレクトリ:end

                        if (processPropertiesFile(manifest,
                                BlancoPropertiesUtil.getBabelURI(manifest,
                                        strPropertyFilePath), inStream, "/"
                                        + (subdir.length() == 0 ? "" : "/"
                                                + subdir) + "/"
                                        + files[index].getName())) {
                            // 置換されました。
                        }

                        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, "/" + entry.getName())) {
                        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,
            final String argTargetPropertiesNameWithPath) 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, manifest,
                reader, argTargetPropertiesNameWithPath);

        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 (fIsProcessTargetLocale) {
            // ターゲット･ロケールが処理対象となる場合にのみ判定します。
            if (argFilename.endsWith("_" + fTargetLocale + ".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 BlancoPropertiesManifest manifest,
            final BufferedReader reader,
            final String argTargetPropertiesNameWithPath) throws IOException {
        boolean isReplaced = false;

        final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        final BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(outStream));

        beginProperties(bundleURI, props, argTargetPropertiesNameWithPath,
                writer);

        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();

                    if (fIsReportNoteToTranslators) {
                        if (line.toLowerCase().indexOf("note to translator") >= 0
                                || line.toLowerCase().indexOf(
                                        "do not translate") >= 0) {
                            if (fReportNoteToTranslatorsResultURI
                                    .get(bundleURI) == null) {
                                // 初回のみ報告します。
                                fReportNoteToTranslatorsResultURI.put(
                                        bundleURI, bundleURI);
                                System.out
                                        .println("properties: to translator: uri["
                                                + bundleURI
                                                + "], ["
                                                + line
                                                + "]");
                            }
                        }
                    }

                    continue;
                }

                final StringBuffer strbufKey = new StringBuffer();

                StringReader readerLine = new StringReader(line);
                for (;;) {
                    final int iRead = readerLine.read();
                    if (iRead < 0) {
                        writer.newLine();
                        break outerLoop;
                    }
                    if (iRead == '\\') {
                        // エスケープ開始。
                        final int iRead2 = readerLine.read();
                        if (iRead2 < 0) {
                            // エスケープ中なのに行末に来てしまいました。
                            // 仕方が無いので何事も無かったことにします。
                            System.out.println(fMsg
                                    .getMbprp001(bundleURI, line));

                            // そもそも、キー読み込み最中に行末に到達するのは困ります。
                            // 今までの実績から、そのようなプロパティファイルは処理すべきはでないものと判断します。
                            // 処理中断します。
                            break;
                        }
                        switch ((char) iRead2) {
                        case 'n':
                            strbufKey.append('\n');
                            break;
                        case 'u':
                            // unicode表現の場合。
                            strbufKey.append("\\u");
                            break;
                        default:
                            strbufKey.append((char) iRead2);
                            break;
                        }
                    } else {
                        // 直接書込。
                        writer.write(iRead);
                        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();

                // 取得されたキーをもとに引き当て可能であることをチェックします。

                // keyに \ が入ることの考慮付きで文字列を取得します。
                final String strKey = BlancoPropertiesUtil
                        .normalizeKeyString(strbufKey.toString());
                if (strKey.trim().length() == 0) {
                    // このファイルは処理できないのでスキップ扱いします。
                    System.out.println(fMsg.getMbprp004(strKey, strbufKey
                            .toString()));
                    return false;
                }
                final String inputValue = (String) props.get(strKey);
                if (inputValue == null) {
                    System.out.println(fMsg.getMbprp002(bundleURI, strKey,
                            strbufKey.toString()));
                    final Iterator<Entry<Object, Object>> ite = props
                            .entrySet().iterator();
                    for (; ite.hasNext();) {
                        final Object obj = ite.next();
                        System.out.println("  " + obj.toString());
                    }

                    // 処理中断。
                    throw new IllegalArgumentException(fMsg.getMbprp003(
                            bundleURI, strKey));
                }

                if (processPropertiesKey(bundleURI, props, strKey, writer)) {
                    isReplaced = true;
                }
            }
        }

        writer.close();

        endProperties(bundleURI, props, manifest, outStream.toByteArray(),
                isReplaced);

        return isReplaced;
    }
}
