/*
 * blanco Framework
 * Copyright (C) 2004-2006 IGA Tosiki
 * 
 * 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.logmessage;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import blanco.cg.BlancoCgObjectFactory;
import blanco.cg.BlancoCgSupportedLang;
import blanco.cg.transformer.BlancoCgTransformerFactory;
import blanco.cg.util.BlancoCgLineUtil;
import blanco.cg.util.BlancoCgSourceUtil;
import blanco.cg.valueobject.BlancoCgClass;
import blanco.cg.valueobject.BlancoCgField;
import blanco.cg.valueobject.BlancoCgMethod;
import blanco.cg.valueobject.BlancoCgSourceFile;
import blanco.commons.util.BlancoNameAdjuster;
import blanco.commons.util.BlancoStringUtil;
import blanco.logmessage.message.BlancoLogMessageMessage;
import blanco.logmessage.resourcebundle.BlancoLogMessageResourceBundle;
import blanco.logmessage.stringgroup.BlancoLogMessageLevelStringGroup;
import blanco.logmessage.stringgroup.BlancoLogMessageLoggerStringGroup;
import blanco.logmessage.valueobject.BlancoLogMessageFieldStructure;
import blanco.logmessage.valueobject.BlancoLogMessageStructure;
import blanco.resourcebundle.BlancoResourceBundleUtil;
import blanco.resourcebundle.BlancoResourceBundleXml2JavaClass;
import blanco.resourcebundle.valueobject.BlancoResourceBundleBundleItemStructure;
import blanco.resourcebundle.valueobject.BlancoResourceBundleBundleResourceStringStructure;
import blanco.resourcebundle.valueobject.BlancoResourceBundleBundleStructure;

/**
 * 「メッセージ定義書」Excel様式からメッセージを処理するクラス・ソースコードを生成。
 * 
 * このクラスは、中間XMLファイルからソースコードを自動生成する機能を担います。
 * 
 * @author IGA Tosiki
 */
public class BlancoLogMessageXml2SourceFile {
    private final BlancoLogMessageMessage fMsg = new BlancoLogMessageMessage();

    /**
     * [key] は予約語とします。
     */
    private static final String KEY = "[key]";

    /**
     * このプロダクトのリソースバンドルへのアクセスオブジェクト。
     */
    private final BlancoLogMessageResourceBundle fBundle = new BlancoLogMessageResourceBundle();

    /**
     * 出力対象となるプログラミング言語。
     */
    private int fTargetLang = BlancoCgSupportedLang.NOT_DEFINED;

    /**
     * 出力対象となるロガー。
     */
    private String fTargetLogger = "java.util.logging";

    /**
     * 内部的に利用するblancoCg用ファクトリ。
     */
    private BlancoCgObjectFactory fCgFactory = null;

    /**
     * 内部的に利用するblancoCg用ソースファイル情報。
     */
    private BlancoCgSourceFile fCgSourceFile = null;

    /**
     * 内部的に利用するblancoCg用クラス情報。
     */
    private BlancoCgClass fCgClass = null;

    /**
     * 自動生成するソースファイルの文字エンコーディング。
     */
    private String fEncoding = null;

    public void setEncoding(final String argEncoding) {
        fEncoding = argEncoding;
    }

    /**
     * 出力対象となるロガーを指定します。
     * 
     * @param argLogger
     *            出力対象となるロガー。
     */
    public void setTargetLogger(final String argLogger) {
        fTargetLogger = argLogger;
    }

    /**
     * 中間XMLファイルからソースコードを自動生成します。
     * 
     * @param argMetaXmlSourceFile
     *            メタ情報が含まれているXMLファイル。
     * @param argTargetLang
     *            出力対象プログラミング言語。
     * @param argDirectoryTarget
     *            ソースコード生成先ディレクトリ (/mainを除く部分を指定します)。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public void process(final File argMetaXmlSourceFile,
            final String argTargetLang, final File argDirectoryTarget)
            throws IOException {

        fTargetLang = new BlancoCgSupportedLang().convertToInt(argTargetLang);
        switch (fTargetLang) {
        case BlancoCgSupportedLang.JAVA:
            break;
        case BlancoCgSupportedLang.CS:
        case BlancoCgSupportedLang.JS:
        case BlancoCgSupportedLang.VB:
        case BlancoCgSupportedLang.PHP:
        case BlancoCgSupportedLang.RUBY:
        case BlancoCgSupportedLang.PYTHON:
        default:
            throw new IllegalArgumentException(fMsg.getMbmsgi02(argTargetLang));
        }

        final BlancoLogMessageStructure[] structures = new BlancoLogMessageXmlParser()
                .parse(argMetaXmlSourceFile);

        for (int index = 0; index < structures.length; index++) {
            // メタ情報の解析結果をもとにソースコード自動生成を実行します。
            structure2Source(structures[index], argDirectoryTarget);

            if (fTargetLang == BlancoCgSupportedLang.JAVA) {
                expandResourceBundleForJava(structures[index],
                        argDirectoryTarget);
            }
        }
    }

    /**
     * 指定のシートの記述内容を展開します。
     * 
     * @param argStructure
     *            タスクの構造。
     * @param argDirectoryTarget
     *            出力先ディレクトリ
     */
    public void structure2Source(final BlancoLogMessageStructure argStructure,
            final File argDirectoryTarget) {
        // 従来と互換性を持たせるため、/mainサブフォルダに出力します。
        final File fileBlancoMain = new File(argDirectoryTarget
                .getAbsolutePath()
                + "/main");

        fCgFactory = BlancoCgObjectFactory.getInstance();
        fCgSourceFile = fCgFactory.createSourceFile(argStructure.getPackage(),
                "このソースコードは blanco Frameworkによって自動生成されています。");
        fCgSourceFile.setEncoding(fEncoding);
        fCgClass = fCgFactory.createClass(BlancoNameAdjuster
                .toClassName(argStructure.getName())
                + BlancoStringUtil.null2Blank(argStructure.getSuffix()),
                BlancoStringUtil.null2Blank(argStructure.getDescription()));
        fCgSourceFile.getClassList().add(fCgClass);

        if (fTargetLang == BlancoCgSupportedLang.JAVA) {
            // Javaの場合にはリソースバンドルを利用。
            final BlancoCgField field = new BlancoCgField();
            field.setName("fBundle");
            field.setType(fCgFactory.createType(argStructure.getPackage() + "."
                    + BlancoNameAdjuster.toClassName(argStructure.getName())
                    + argStructure.getSuffix() + "ResourceBundle"));
            field
                    .setDescription("ログメッセージをプロパティファイル対応させるための内部的に利用するリソースバンドルクラス。");
            field.setDefault("new "
                    + BlancoNameAdjuster.toClassName(argStructure.getName())
                    + argStructure.getSuffix() + "ResourceBundle()");
            field.setFinal(true);
            field.setAccess("protected");
            fCgClass.getFieldList().add(field);
        }

        for (int indexField = 0; indexField < argStructure.getFieldList()
                .size(); indexField++) {
            final BlancoLogMessageFieldStructure fieldLook = argStructure
                    .getFieldList().get(indexField);
            expandMethodLogMessage(argStructure, fieldLook, false);
            expandMethodLogMessage(argStructure, fieldLook, true);
        }

        BlancoCgTransformerFactory.getSourceTransformer(fTargetLang).transform(
                fCgSourceFile, fileBlancoMain);
    }

    /**
     * logメッセージ メソッドを展開します。
     * 
     * @param argProcessStructure
     *            メタファイルから収集できた処理構造データ。
     * @param argThrowable
     *            例外付きかどうか。
     */
    @SuppressWarnings("unchecked")
    private void expandMethodLogMessage(
            final BlancoLogMessageStructure argProcessStructure,
            final BlancoLogMessageFieldStructure fieldLook,
            final boolean argThrowable) {

        final List listSplitMessage = new ArrayList();

        if (argProcessStructure.getIdEmbedding() == false) {
            listSplitMessage.add(fieldLook.getMessage());
        } else {
            if (fieldLook.getMessage().indexOf(KEY) < 0) {
                listSplitMessage.add("[" + fieldLook.getName() + "] "
                        + fieldLook.getMessage());
            } else {
                // 指定箇所に埋め込み。
                listSplitMessage.add(BlancoStringUtil.replace(fieldLook
                        .getMessage(), KEY, "[" + fieldLook.getName() + "]",
                        true));
            }
        }

        final int maxReplaceNumber = new BlancoLogMessageSplitUtil()
                .split(listSplitMessage);

        final int blancoResourceBundleFormatCount = BlancoResourceBundleUtil
                .getFormatsByArgumentIndex(fieldLook.getMessage(), false).length;

        if (maxReplaceNumber + 1 != blancoResourceBundleFormatCount) {
            throw new IllegalArgumentException(fMsg.getMbmsgi04(
                    argProcessStructure.getName(), fieldLook.getName(), Integer
                            .toString(maxReplaceNumber + 1), Integer
                            .toString(blancoResourceBundleFormatCount)));
        }

        final String methodName = "log"
                + BlancoNameAdjuster.toClassName(fieldLook.getName());

        final BlancoCgMethod cgMethod = fCgFactory.createMethod(
                getMethodName(methodName), "ログメッセージ定義ID["
                        + argProcessStructure.getName() + "]、レベル["
                        + fieldLook.getLevel() + "]、キー[" + fieldLook.getName()
                        + "]の文字列をロギングします。");
        fCgClass.getMethodList().add(cgMethod);

        // System.out.println("置換文字列数:" + maxReplaceNumber);
        for (int indexArg = 0; indexArg <= maxReplaceNumber; indexArg++) {
            cgMethod.getParameterList().add(
                    fCgFactory.createParameter("arg" + indexArg,
                            getTypeString(), "置換文字列{" + indexArg + "}の値。"));
        }

        if (argThrowable) {
            cgMethod.getParameterList().add(
                    fCgFactory.createParameter("argThrown",
                            "java.lang.Throwable", "例外。"));
        }

        if (fieldLook.getNo() != null) {
            cgMethod.getLangDoc().getDescriptionList().add(
                    fBundle.getXml2sourceFileFieldNo(fieldLook.getNo()));
        }
        cgMethod.getLangDoc().getDescriptionList().add(
                "文字列["
                        + BlancoCgSourceUtil.escapeStringAsLangDoc(fTargetLang,
                                fieldLook.getMessage()) + "]");

        final List<java.lang.String> lineList = cgMethod.getLineList();

        final StringBuffer bufLine = new StringBuffer();

        String logLevelName = "INFO";
        String sysoutLevelName = "INFO";
        String logMessagePrefix = "";
        switch (new BlancoLogMessageLevelStringGroup().convertToInt(fieldLook
                .getLevel())) {
        case BlancoLogMessageLevelStringGroup.FATAL:
            logMessagePrefix = "fatal: ";
            logLevelName = "SEVERE";
            sysoutLevelName = "FATAL";
            break;
        case BlancoLogMessageLevelStringGroup.ERROR:
            logLevelName = "SEVERE";
            sysoutLevelName = "ERROR";
            break;
        case BlancoLogMessageLevelStringGroup.WARN:
            logLevelName = "WARNING";
            sysoutLevelName = "WARN ";
            break;
        case BlancoLogMessageLevelStringGroup.INFO:
            logLevelName = "INFO";
            sysoutLevelName = "INFO ";
            break;
        case BlancoLogMessageLevelStringGroup.CONF:
            logLevelName = "CONFIG";
            sysoutLevelName = "CONF ";
            break;
        case BlancoLogMessageLevelStringGroup.DEBUG:
            logLevelName = "FINE";
            sysoutLevelName = "DEBUG";
            break;
        case BlancoLogMessageLevelStringGroup.TRACE:
            logLevelName = "FINER";
            sysoutLevelName = "TRACE";
            break;
        }

        if (new BlancoLogMessageLoggerStringGroup().convertToString(
                BlancoLogMessageLoggerStringGroup.LOG4J).equals(fTargetLogger)) {
            // Log4j の場合には レベルを上書き。
            switch (new BlancoLogMessageLevelStringGroup()
                    .convertToInt(fieldLook.getLevel())) {
            case BlancoLogMessageLevelStringGroup.FATAL:
                logMessagePrefix = "";
                logLevelName = "FATAL";
                break;
            case BlancoLogMessageLevelStringGroup.ERROR:
                logLevelName = "ERROR";
                break;
            case BlancoLogMessageLevelStringGroup.WARN:
                logLevelName = "WARN";
                break;
            case BlancoLogMessageLevelStringGroup.INFO:
                logLevelName = "INFO";
                break;
            case BlancoLogMessageLevelStringGroup.CONF:
                logMessagePrefix = "config: ";
                logLevelName = "DEBUG";
                break;
            case BlancoLogMessageLevelStringGroup.DEBUG:
                logLevelName = "DEBUG";
                break;
            case BlancoLogMessageLevelStringGroup.TRACE:
                logLevelName = "TRACE";
                break;
            }
        }

        final StringBuffer buf = new StringBuffer();
        if (argProcessStructure.getIdEmbedding()) {
            if (fieldLook.getMessage().indexOf(KEY) < 0) {
                // ID埋め込み
                buf
                        .append(BlancoCgLineUtil
                                .getStringLiteralEnclosure(fTargetLang)
                                + "["
                                + fieldLook.getName()
                                + "] "
                                + logMessagePrefix
                                + BlancoCgLineUtil
                                        .getStringLiteralEnclosure(fTargetLang)
                                + " + ");
            } else {
                if (logMessagePrefix.length() > 0) {
                    // 長さがある場合にのみセット。
                    buf.append(BlancoCgLineUtil
                            .getStringLiteralEnclosure(fTargetLang)
                            + logMessagePrefix
                            + BlancoCgLineUtil
                                    .getStringLiteralEnclosure(fTargetLang)
                            + " + ");
                }
            }
        } else {
            if (logMessagePrefix.length() > 0) {
                buf
                        .append(BlancoCgLineUtil
                                .getStringLiteralEnclosure(fTargetLang)
                                + logMessagePrefix
                                + BlancoCgLineUtil
                                        .getStringLiteralEnclosure(fTargetLang)
                                + " + ");
            }
        }
        buf.append("fBundle.get"
                + BlancoNameAdjuster.toClassName(fieldLook.getName()) + "(");
        for (int indexArg = 0; indexArg <= maxReplaceNumber; indexArg++) {
            if (indexArg != 0) {
                buf.append(", ");
            }
            buf.append("arg" + indexArg);
        }
        buf.append(")");
        bufLine.append(buf.toString());

        fCgSourceFile.getImportList().add(fTargetLogger + ".Level");
        fCgSourceFile.getImportList().add(fTargetLogger + ".Logger");

        if (argProcessStructure.getStdout()) {
            lineList.add("System.out.println(\"" + sysoutLevelName + ": \" + "
                    + bufLine.toString()
                    + (argThrowable ? " + \": \" + argThrown.toString()" : "")
                    + ")" + BlancoCgLineUtil.getTerminator(fTargetLang));
        }

        lineList.add("Logger.getLogger(\"" + argProcessStructure.getPackage()
                + "\").log(Level." + logLevelName + ", " + bufLine.toString()
                + (argThrowable ? ", argThrown" : "") + ")"
                + BlancoCgLineUtil.getTerminator(fTargetLang));
    }

    /**
     * プログラミング言語処理系に合った String 型の名称を取得します。
     * 
     * 型の読み替え。
     * 
     * @return
     */
    private final String getTypeString() {
        switch (fTargetLang) {
        case BlancoCgSupportedLang.JAVA:
        default:
            return "java.lang.String";
        case BlancoCgSupportedLang.CS:
            return "string";
        case BlancoCgSupportedLang.JS:
        case BlancoCgSupportedLang.PHP:
            return "string";
        case BlancoCgSupportedLang.VB:
            return "String";
        }
    }

    /**
     * プログラミング言語処理系に合った メソッドの名前変形をおこないます。
     * 
     * メソッド名の読み替え。
     * 
     * @return
     */
    private final String getMethodName(final String argMethodName) {
        switch (fTargetLang) {
        case BlancoCgSupportedLang.JAVA:
        default:
            return argMethodName;
        case BlancoCgSupportedLang.CS:
        case BlancoCgSupportedLang.VB:
            return BlancoNameAdjuster.toUpperCaseTitle(argMethodName);
        case BlancoCgSupportedLang.JS:
            return argMethodName;
        }
    }

    /**
     * Javaの場合にリソースバンドルクラスを生成します。
     * 
     * @param argStructure
     * @param argDirectoryTarget
     */
    private void expandResourceBundleForJava(
            final BlancoLogMessageStructure argStructure,
            final File argDirectoryTarget) {
        final BlancoResourceBundleBundleStructure bundleStructure = new BlancoResourceBundleBundleStructure();
        bundleStructure.setName(BlancoNameAdjuster.toClassName(argStructure
                .getName())
                + argStructure.getSuffix());
        bundleStructure.setPackage(argStructure.getPackage());
        // クラスをパッケージアクセスに制限します。
        bundleStructure.setAccess("");
        bundleStructure.setSuffix("ResourceBundle");
        bundleStructure.setDescription("メッセージ定義[" + argStructure.getName()
                + "]が内部的に利用するリソースバンドルクラス。");

        // TODO デフォルトロケールを利用することにしています。
        final Locale defalutLocale = Locale.getDefault();

        for (int index = 0; index < argStructure.getFieldList().size(); index++) {
            final BlancoLogMessageFieldStructure fieldStructure = (BlancoLogMessageFieldStructure) argStructure
                    .getFieldList().get(index);
            final BlancoResourceBundleBundleItemStructure item = new BlancoResourceBundleBundleItemStructure();
            item.setKey(fieldStructure.getName());
            final BlancoResourceBundleBundleResourceStringStructure resourceString = new BlancoResourceBundleBundleResourceStringStructure();

            {
                // キーの置換。
                String message = BlancoStringUtil.null2Blank(fieldStructure
                        .getMessage());
                if (argStructure.getIdEmbedding() && message.indexOf(KEY) >= 0) {
                    // 指定箇所に埋め込み。
                    message = BlancoStringUtil.replace(message, KEY, "["
                            + item.getKey() + "]", true);
                }
                resourceString.setResourceString(message);
            }

            resourceString.setLocale(defalutLocale.getLanguage());
            item.getResourceStringList().add(resourceString);
            bundleStructure.getItemList().add(item);
        }

        final BlancoResourceBundleXml2JavaClass xml2SourceFile = new BlancoResourceBundleXml2JavaClass();
        xml2SourceFile.setEncoding(fEncoding);
        xml2SourceFile.structure2Source(bundleStructure, argDirectoryTarget);
    }
}
